Skip to content

feat: add UTM parameter generator to LinkEditForm#4270

Open
KAUSHALCODER123 wants to merge 1 commit into
umami-software:masterfrom
KAUSHALCODER123:feat/utm-generator
Open

feat: add UTM parameter generator to LinkEditForm#4270
KAUSHALCODER123 wants to merge 1 commit into
umami-software:masterfrom
KAUSHALCODER123:feat/utm-generator

Conversation

@KAUSHALCODER123

Copy link
Copy Markdown

Changes:
Added a togglable UTM generator section to the Link Add/Edit form.
Automatic parsing of UTM parameters from existing URLs.
Real-time synchronization between UTM fields and the Destination URL.
Automatic filtering of helper fields before form submission

@vercel

vercel Bot commented May 10, 2026

Copy link
Copy Markdown

@KAUSHALCODER123 is attempting to deploy a commit to the Umami Software Team on Vercel.

A member of the Team first needs to authorize it.

@KAUSHALCODER123

Copy link
Copy Markdown
Author

accept my pr

@greptile-apps

greptile-apps Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a collapsible UTM parameter generator to the LinkEditForm, allowing users to compose UTM query params via dedicated fields that sync into the destination URL in real time. UTM fields are stripped from the form payload before submission so only the final URL is persisted.

  • UTM fields (source, medium, campaign, content, term) are toggled via a button next to the URL field, and the form parses any existing UTM params from an edit target's URL to pre-populate them.
  • Each UTM field pushes its value directly into the url form field on change, keeping the destination URL up to date without requiring manual editing.
  • The handleSubmit function destructures and discards the helper fields before calling mutateAsync, ensuring no schema changes are needed.

Confidence Score: 3/5

The UTM section's auto-expand feature is broken for existing links and the UTM fields can silently overwrite manual URL edits; these should be fixed before merging.

The showUtm initializer always captures undefined because useLinkQuery hasn't resolved at mount time, so the described automatic parsing of UTM parameters from existing URLs never fires. Additionally, editing the URL field directly leaves the UTM helper fields stale; the next change to any UTM field will overwrite whatever the user typed. Both issues affect the core interaction path of the feature.

src/app/(main)/links/LinkEditForm.tsx — specifically the showUtm initialization and the missing URL-to-UTM-field sync

Important Files Changed

Filename Overview
src/app/(main)/links/LinkEditForm.tsx Adds UTM generator UI; showUtm always initializes to false because useState runs before useLinkQuery resolves, and there is no URL→UTM reverse sync for manually edited URLs.

Sequence Diagram

sequenceDiagram
    participant User
    participant Form
    participant UTMField
    participant URLField
    participant API

    User->>Form: Open edit form (existing link)
    Form->>API: useLinkQuery(linkId)
    API-->>Form: data (url with utm params)
    Note over Form: useState initializer already ran with data=undefined - showUtm=false (BUG)

    User->>Form: Toggle UTM section (button)
    Form->>Form: setShowUtm(true)

    User->>UTMField: Type value
    UTMField->>URLField: setValue url with utm param
    Note over URLField: URL field updated

    User->>URLField: Manually edit URL
    URLField-->>UTMField: No update - stale UTM fields (BUG)

    User->>Form: Submit
    Form->>Form: Strip utmSource/Medium/etc from payload
    Form->>API: mutateAsync with updated url
    API-->>Form: success
Loading

Reviews (1): Last reviewed commit: "feat: add UTM parameter generator to Lin..." | Re-trigger Greptile

Comment on lines +65 to +68
const [showUtm, setShowUtm] = useState(() => {
const utm = parseUtm(data?.url);
return !!(utm.utmSource || utm.utmMedium || utm.utmCampaign || utm.utmContent || utm.utmTerm);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 showUtm initializer always sees undefined data

useState's initializer runs once on mount, but at that point useLinkQuery hasn't resolved yet — data is undefined. So parseUtm(undefined) returns {}, and showUtm is always initialized to false. When the query does resolve and the <Loading> early-return lifts, useState does not re-run its initializer. As a result, the UTM section never auto-expands for existing links that already contain UTM params in their URL, breaking the "automatic parsing from existing URLs" behavior described in the PR.

Suggested change
const [showUtm, setShowUtm] = useState(() => {
const utm = parseUtm(data?.url);
return !!(utm.utmSource || utm.utmMedium || utm.utmCampaign || utm.utmContent || utm.utmTerm);
});
const [showUtm, setShowUtm] = useState(false);
useEffect(() => {
if (data?.url) {
const utm = parseUtm(data.url);
if (utm.utmSource || utm.utmMedium || utm.utmCampaign || utm.utmContent || utm.utmTerm) {
setShowUtm(true);
}
}
}, [data]);

Comment on lines +127 to +206
<FormField label={t(labels.utmSource)} name="utmSource">
<TextField
autoComplete="off"
onChange={val => {
const url = watch('url');
try {
const u = new URL(url);
if (val) u.searchParams.set('utm_source', val);
else u.searchParams.delete('utm_source');
setValue('url', u.toString(), { shouldDirty: true });
} catch {
// ignore
}
}}
/>
</FormField>
<FormField label={t(labels.utmMedium)} name="utmMedium">
<TextField
autoComplete="off"
onChange={val => {
const url = watch('url');
try {
const u = new URL(url);
if (val) u.searchParams.set('utm_medium', val);
else u.searchParams.delete('utm_medium');
setValue('url', u.toString(), { shouldDirty: true });
} catch {
// ignore
}
}}
/>
</FormField>
<FormField label={t(labels.utmCampaign)} name="utmCampaign">
<TextField
autoComplete="off"
onChange={val => {
const url = watch('url');
try {
const u = new URL(url);
if (val) u.searchParams.set('utm_campaign', val);
else u.searchParams.delete('utm_campaign');
setValue('url', u.toString(), { shouldDirty: true });
} catch {
// ignore
}
}}
/>
</FormField>
<FormField label={t(labels.utmContent)} name="utmContent">
<TextField
autoComplete="off"
onChange={val => {
const url = watch('url');
try {
const u = new URL(url);
if (val) u.searchParams.set('utm_content', val);
else u.searchParams.delete('utm_content');
setValue('url', u.toString(), { shouldDirty: true });
} catch {
// ignore
}
}}
/>
</FormField>
<FormField label={t(labels.utmTerm)} name="utmTerm" colSpan="2">
<TextField
autoComplete="off"
onChange={val => {
const url = watch('url');
try {
const u = new URL(url);
if (val) u.searchParams.set('utm_term', val);
else u.searchParams.delete('utm_term');
setValue('url', u.toString(), { shouldDirty: true });
} catch {
// ignore
}
}}
/>
</FormField>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 No reverse sync from URL field to UTM fields

Every UTM TextField pushes its value into the url field (UTM → URL), but the reverse path is absent. If a user manually edits the url field and changes or adds a UTM query param directly, the individual UTM fields (utmSource, utmMedium, etc.) remain stale and will silently overwrite whatever the user typed when they next touch one of those fields.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant